/*
 * Copyright 2024 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.netflix.maestro.utils;

import com.netflix.maestro.annotations.Nullable;
import com.netflix.maestro.models.Constants;
import com.netflix.maestro.models.definition.Workflow;
import com.netflix.maestro.models.trigger.TriggerUuids;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/** Utility class for Maestro to generate various ids. */
public final class IdHelper {

  private static final char[] BASE62_CHARS =
      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();
  private static final int BASE = BASE62_CHARS.length;

  private IdHelper() {}

  /** Return the input uuid string if not null. Otherwise, generate a random one. */
  public static String getOrCreateUuid(@Nullable UUID uuid) {
    if (uuid != null) {
      return uuid.toString();
    }
    return UUID.randomUUID().toString();
  }

  /** Return UUID generated by the input string. If null, return a random one. */
  public static UUID createUuid(String input) {
    if (input == null) {
      return UUID.randomUUID();
    }
    return UUID.nameUUIDFromBytes(input.getBytes(StandardCharsets.UTF_8));
  }

  /** static method to create TriggerUuids from a workflow. */
  public static TriggerUuids toTriggerUuids(Workflow workflow) {
    TriggerUuids.TriggerUuidsBuilder builder = TriggerUuids.builder();
    if (workflow.getTimeTriggers() != null && !workflow.getTimeTriggers().isEmpty()) {
      builder.timeTriggerUuid(
          IdHelper.createUuid(workflow.getTimeTriggers().toString()).toString());
    }
    if (workflow.getSignalTriggers() != null && !workflow.getSignalTriggers().isEmpty()) {
      builder.signalTriggerUuids(
          IntStream.range(0, workflow.getSignalTriggers().size())
              .boxed()
              .collect(
                  Collectors.toMap(
                      e ->
                          IdHelper.createUuid(workflow.getSignalTriggers().get(e).toString())
                              .toString(),
                      Function.identity())));
    }
    return builder.build();
  }

  /** encode a long value into a hash key. */
  public static String hashKey(long value) {
    return encodeBase62(value, false);
  }

  /**
   * encode a long value into a range key with predefined constant length MAX_RANGE_KEY_LENGTH. We
   * use variable length
   */
  public static String rangeKey(long value) {
    return encodeBase62(value, true);
  }

  /** assemble the inline workflow id prefix based on workflow internal id. */
  public static String getInlineWorkflowPrefixId(long workflowInternalId) {
    return String.format(
        "%s_%s_", Constants.FOREACH_INLINE_WORKFLOW_PREFIX, hashKey(workflowInternalId));
  }

  /** determine if the given workflow id is for an inline workflow. */
  public static boolean isInlineWorkflowId(String workflowId) {
    return workflowId.startsWith(Constants.FOREACH_INLINE_WORKFLOW_PREFIX);
  }

  /**
   * Encode a long value into a compact base62 string using some special algorithm. For the case
   * (e.g. range key) if the string must be preserving the natural ordering, the value will be
   * encoded into base62 and then add its length as the prefix to the string. In this way, we
   * support variable length base62 encoding with the original natural ordering. e.g. 62 (base62:
   * 11) vs 9 (base62: 9) -> 211 vs 19.
   *
   * @param value value to encode
   * @param isOrdered should the output encoded string perserve the ordering. True for rangeKey case
   *     and false for hashKey.
   * @return encoded base62 string
   */
  private static String encodeBase62(long value, boolean isOrdered) {
    Checks.checkTrue(value > 0, "Input value must be positive: %s", value);

    StringBuilder sb = new StringBuilder();
    long val = value; // avoid pmd error
    while (val != 0) {
      sb.append(BASE62_CHARS[(int) (val % BASE)]);
      val /= BASE;
    }

    if (isOrdered) {
      sb.append(BASE62_CHARS[sb.length()]);
      sb.reverse();
    }
    return sb.toString();
  }
}
